The world of web development is marked by constant innovation, and developers are always on the lookout for tools and libraries that can simplify their workflows and make cross-platform development more efficient. In this article, we’ll explore a powerful combination of technologies: React Native Expo, Next.js Server-Side Rendering (SSR), and some handy libraries like Fernando Rojo’s Solito and Zustand with Persist Middleware. Together, these technologies enable the creation of cross-platform applications with universal storage capabilities.
Bridging the Gap with Solito
At the heart of this cross-platform development approach is Solito, a library created by Fernando Rojo. Solito acts as the missing link that seamlessly bridges the gap between React Native and Next.js, enabling developers to build powerful cross-platform applications. It serves two primary purposes:
- Navigation Simplified: Solito provides a tiny wrapper around React Navigation and Next.js, making it easy to share navigation code between native and web platforms. This means you can define your navigation logic once and use it across both platforms.
- Patterns and Examples: Solito offers a set of patterns and examples that guide developers in building cross-platform apps with React Native and Next.js. It simplifies the development process by providing best practices and clear examples.
Zustand with Persist Middleware
A crucial aspect of cross-platform development is managing state effectively. To achieve this, we turn to Zustand, a state management library. Zustand, when coupled with the Persist Middleware, enables us to persist and manage application state consistently across platforms.
Creating a Starter Project
To kickstart your journey into universal storage for React Native Expo + Next.js SSR, you can follow the guide provided by Solito: Solito Starter Project. This guide will help you set up the foundational structure of your project.
Universal Persisted Storage Implementation
In the /packages/app/storage.ts
file, we implement persist storage that utilizes cookies for Next.js/web and React Native MMKV on the native side. This setup allows your data to be accessible on the server side and in React Native:
1import Cookies from 'js-cookie'
2import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string'
3import { Platform } from 'react-native'
4import { MMKV } from 'react-native-mmkv'
5import { createJSONStorage, StateStorage } from 'zustand/middleware'
6const mmkv = new MMKV()
7const MMKVStorage: StateStorage = {
8 getItem: (name) => {
9 const value = mmkv.getString(name)
10 if (!value)
11 return null
12 return value
13 },
14 setItem: (name, value) => {
15 mmkv.set(name, value)
16 },
17 removeItem: (name) => {
18 mmkv.delete(name)
19 },
20}
21const CookieStorage: StateStorage = {
22 getItem: (key) => {
23 const value = Cookies.get(key)
24 if (!value) return null
25 return decompressFromEncodedURIComponent(value)
26 },
27 setItem: (key, value) => {
28 Cookies.set(key, compressToEncodedURIComponent(value))
29 },
30 removeItem: (key) => { Cookies.remove(key) },
31}
32export const storage = {
33 create: <T>() => {
34 return createJSONStorage<T>(() => {
35 if (Platform.OS !== 'web')
36 return MMKVStorage return CookieStorage
37 })
38 },
39}
Zustand Store
We create a Zustand store in /packages/app/stores/main.ts
, where we define the state we want to persist. In this example, we’re persisting the disableParallaxEffect key:
1import { create } from 'zustand'
2import { persist } from 'zustand/middleware'
3
4import { storage } from './storage'
5export namespace Main {
6 export type State = {
7 webHeaderHeight: number disableParallaxEffect: boolean
8 }
9}
10export const main = create<Main.State>()(
11 persist( (set, get) => ({
12 webHeaderHeight: 0,
13 disableParallaxEffect: false,
14 }),
15 { name: 'main-storage',
16 storage: storage.create(),
17 partialize: (state) => ({ disableParallaxEffect: state.disableParallaxEffect, }),
18 }
19 )
20)
Addressing Hydration Mismatches
A challenge when persisting state in Next.js SSR is dealing with hydration mismatches. To address this issue, we make adjustments in the /apps/next/pages/_app.tsx
file. This code ensures that the persisted state is correctly restored during server-side rendering:
1App.getInitialProps = async (app: AppContext) => { const appProps = await NextApp.getInitialProps(app) const { req } = app.ctx if (req) { // if we're on the server const cookieStrings = req.headers.cookie || '' const cookies = Object.fromEntries( cookieStrings.split('; ')?.map((v) => v.split(/=(.*)/s)?.map(decodeURIComponent)) ) // Get and parse persisted main-storage cookie const mainStorageCookie = cookies['main-storage'] const mainStorage = JSON.parse( mainStorageCookie ? decompressFromEncodedURIComponent(mainStorageCookie) : `{"state":{"disableParallaxEffect":false},"version":0}` ) as { state: { disableParallaxEffect: boolean } version: number } if (mainStorage.state) { // Set main state main.setState(mainStorage.state) } } return { ...appProps }}
In this article, we’ve explored the fascinating world of universal storage for React Native Expo + Next.js SSR. By leveraging libraries like Solito, Zustand, and Persist Middleware, we can build cross-platform applications with shared state management, ensuring a consistent user experience across web and native platforms. The journey into cross-platform development continues to evolve, and these tools and patterns empower developers to tackle the challenges that come their way.
Feel free to explore and experiment with these technologies to enhance your cross-platform development projects. As always, stay curious and keep pushing the boundaries of what you can achieve in the world of web and mobile app development.